package com.wujunshen.redis;

import static com.wujunshen.redis.custom.AbstractRedisCustomizer.getJackson2JsonRedisSerializer;

import com.wujunshen.redis.custom.RedisCustomizer;
import com.wujunshen.redis.properties.RedisPoolProperties;
import com.wujunshen.redis.properties.RedisProperties;
import com.wujunshen.redis.wrapper.MyRedisTemplate;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisPoolingClientConfigurationBuilder;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.StringUtils;
import redis.clients.jedis.JedisPoolConfig;

/**
 * @author frank woo(吴峻申) <br>
 * email:<a href="mailto:frank_wjs@hotmail.com">frank_wjs@hotmail.com</a> <br>
 * @date 2020/2/7 5:33 下午 <br>
 */
@Slf4j
@Configuration
@AutoConfigureBefore(RedisAutoConfiguration.class)
@EnableConfigurationProperties({RedisProperties.class, RedisPoolProperties.class})
@EnableCaching(proxyTargetClass = true)
@ConditionalOnClass({MyRedisTemplate.class, RedisTemplate.class, JedisConnectionFactory.class})
public class MyRedisConfiguration extends RedisClusterConfiguration {
    @Value("${spring.application.name}")
    private String applicationName;
    
    @Resource
    private RedisProperties redisProperties;
    
    @Resource
    private RedisPoolProperties redisPoolProperties;
    
    /**
     * 初始化缓存配置
     *
     * @param customizers 传入的自定义缓存
     * @return Map<String, RedisCacheConfiguration> key是缓存名，value为redis的缓存配置
     */
    public static Map<String, RedisCacheConfiguration> initialCacheConfigurations(
            List<RedisCustomizer> customizers) {
        Map<String, RedisCacheConfiguration> configMap = new ConcurrentHashMap<>(16);
        Map<String, RedisCustomizer> customizerMap = new ConcurrentHashMap<>(16);
        
        // 开始判断customizers是否有另外的RedisCacheConfiguration配置,若有则加入configMap
        if (customizers == null) {
            return configMap;
        }
        for (RedisCustomizer customizer : customizers) {
            Map<String, RedisCacheConfiguration> map = customizer.customize();
            if (map == null) {
                continue;
            }
            for (Map.Entry<String, RedisCacheConfiguration> entry : map.entrySet()) {
                String cacheName = entry.getKey();
                if (StringUtils.isEmpty(cacheName)) {
                    continue;
                }
                if (!configMap.containsKey(cacheName)) {
                    configMap.put(cacheName, entry.getValue());
                    customizerMap.put(cacheName, customizer);
                    log.info("\n已注册缓存配置: {}, 过期时间:{}s\n", cacheName, entry.getValue().getTtl().getSeconds());
                } else {
                    throw new RuntimeException(
                            String.format(
                                    "注册缓存配置重复: %s, %s, %s",
                                    cacheName, customizerMap.get(cacheName).getClass(), customizer.getClass()));
                }
            }
        }
        return configMap;
    }
    
    /**
     * jedis客户端配置
     *
     * @return jedisClientConfiguration
     */
    private JedisClientConfiguration jedisClientConfiguration() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        
        // 最大连接数
        jedisPoolConfig.setMaxTotal(redisPoolProperties.getMaxActive());
        // 最小空闲连接数
        jedisPoolConfig.setMaxIdle(redisPoolProperties.getMinIdle());
        // 最大空闲连接数
        jedisPoolConfig.setMinIdle(redisPoolProperties.getMaxIdle());
        // 当池内没有可用的连接时，最大等待时间
        jedisPoolConfig.setMaxWaitMillis(redisPoolProperties.getMaxWait());
        
        // 通过构造器来构造jedis客户端配置
        return ((JedisPoolingClientConfigurationBuilder) JedisClientConfiguration.builder())
                .poolConfig(jedisPoolConfig)
                .and()
                .connectTimeout(Duration.ofMillis(redisProperties.getTimeOut()))
                .readTimeout(Duration.ofMillis(redisProperties.getTimeOut()))
                .build();
    }
    
    /**
     * redis集群客户端配置
     *
     * @return RedisClusterConfiguration
     */
    private RedisClusterConfiguration redisClusterConfiguration() {
        RedisClusterConfiguration redisClusterConfiguration =
                new RedisClusterConfiguration(redisProperties.getNodes());
        
        Optional.ofNullable(redisProperties.getMaxRedirects())
                .ifPresent(redisClusterConfiguration::setMaxRedirects);
        
        Optional.ofNullable(redisProperties.getPassword())
                .ifPresent(password -> redisClusterConfiguration.setPassword(RedisPassword.of(password)));
        
        return redisClusterConfiguration;
    }
    
    /**
     * 如果配置文件包含“spring.redis”前缀, 则此bean初始化会失效 <br>
     * 应用系统会启动报错
     *
     * @return RedisConnectionFactory
     */
    @Bean
    @ConditionalOnExpression(
            "#{!T(com.wujunshen.redis.support.PropertySourcesSupport).containsPropertyNamePrefix(environment, 'spring" +
                    ".redis')}")
    public RedisConnectionFactory connectionFactory() {
        return new JedisConnectionFactory(redisClusterConfiguration(), jedisClientConfiguration());
    }
    
    @Bean
    public RedisTemplate<Object, Object> redisTemplate() {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        
        redisTemplate.setConnectionFactory(connectionFactory());
        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(getJackson2JsonRedisSerializer());
        // 使用StringRedisSerializer来序列化和反序列化redis的HashKey值
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(getJackson2JsonRedisSerializer());
        
        redisTemplate.afterPropertiesSet();
        
        return redisTemplate;
    }
    
    @Bean
    public MyRedisTemplate myRedisTemplate() {
        return new MyRedisTemplate();
    }
    
    /**
     * 要启用spring缓存支持,需创建一个 CacheManager的 bean，CacheManager 接口有很多实现，这里Redis 的集成，用
     * RedisCacheManager这个实现类 Redis 不是应用的共享内存，它只是一个内存服务器，就像 MySql 似的，
     * 我们需要将应用连接到它并使用某种“语言”进行交互，因此我们还需要一个连接工厂以及一个 Spring 和 Redis 对话要用的 RedisTemplate， 这些都是 Redis
     * 缓存所必需的配置，把它们都放在自定义的 CachingConfigurerSupport 中
     */
    @Bean
    public CacheManager cacheManager(ObjectProvider<List<RedisCustomizer>> customizers) {
        Map<String, RedisCacheConfiguration> configurationMap =
                initialCacheConfigurations(customizers.getIfAvailable());
        
        return new RedisCacheManager(
                RedisCacheWriter.nonLockingRedisCacheWriter(Objects.requireNonNull(connectionFactory())),
                buildCacheConfiguration(),
                configurationMap);
    }
    
    private RedisCacheConfiguration buildCacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(10))
                .serializeValuesWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(
                                getJackson2JsonRedisSerializer()))
                .computePrefixWith(cacheName -> applicationName + ":" + cacheName + ":");
    }
}
